/* @flow */

import {createPromiseCallback} from '../util'
import {createBundleRunner} from './create-bundle-runner'
import type {Renderer, RenderOptions} from '../create-renderer'
import {createSourceMapConsumers, rewriteErrorTrace} from './source-map-support'

const fs = require('fs')
const path = require('path')
const PassThrough = require('stream').PassThrough

const INVALID_MSG =
  'Invalid server-rendering bundle format. Should be a string ' +
  'or a bundle Object of type:\n\n' +
  `{
  entry: string;
  files: { [filename: string]: string; };
  maps: { [filename: string]: string; };
}\n`

// The render bundle can either be a string (single bundled file)
// or a bundle manifest object generated by vue-ssr-webpack-plugin.
type
RenderBundle = {
  basedir? : string;
  entry: string;
  files: {[filename: string
]:
string;
}
;
maps: {
  [filename
:
  string
]:
  string;
}
;
modules ? : {[filename: string
]:
Array < string >
}
;
}
;

export function createBundleRendererCreator(
  createRenderer: (options?: RenderOptions

) =>
Renderer
)
{
  return function createBundleRenderer(
    bundle: string | RenderBundle,
    rendererOptions ? : RenderOptions = {}
)
  {
    let files, entry, maps
    let basedir = rendererOptions.basedir

    // load bundle if given filepath
    if (
      typeof bundle === 'string' &&
      /\.js(on)?$/.test(bundle) &&
      path.isAbsolute(bundle)
    ) {
      if (fs.existsSync(bundle)) {
        const isJSON = /\.json$/.test(bundle)
        basedir = basedir || path.dirname(bundle)
        bundle = fs.readFileSync(bundle, 'utf-8')
        if (isJSON) {
          try {
            bundle = JSON.parse(bundle)
          } catch (e) {
            throw new Error(`Invalid JSON bundle file: ${bundle}`)
          }
        }
      } else {
        throw new Error(`Cannot locate bundle file: ${bundle}`)
      }
    }

    if (typeof bundle === 'object') {
      entry = bundle.entry
      files = bundle.files
      basedir = basedir || bundle.basedir
      maps = createSourceMapConsumers(bundle.maps)
      if (typeof entry !== 'string' || typeof files !== 'object') {
        throw new Error(INVALID_MSG)
      }
    } else if (typeof bundle === 'string') {
      entry = '__vue_ssr_bundle__'
      files = {'__vue_ssr_bundle__': bundle}
      maps = {}
    } else {
      throw new Error(INVALID_MSG)
    }

    const renderer = createRenderer(rendererOptions)

    const run = createBundleRunner(
      entry,
      files,
      basedir,
      rendererOptions.runInNewContext
    )

    return {
      renderToString: (context ? : Object, cb
  :
    any
  ) =>
    {
      if (typeof context === 'function') {
        cb = context
        context = {}
      }

      let promise
      if (!cb) {
        ({promise, cb} = createPromiseCallback())
      }

      run(context).catch(err => {
        rewriteErrorTrace(err, maps)
        cb(err)
      }).then(app => {
        if (app) {
          renderer.renderToString(app, context, (err, res) => {
            rewriteErrorTrace(err, maps)
            cb(err, res)
          })
        }
      })

      return promise
    }
  ,

    renderToStream: (context
    ? : Object
  ) =>
    {
      const res = new PassThrough()
      run(context).catch(err => {
        rewriteErrorTrace(err, maps)
        // avoid emitting synchronously before user can
        // attach error listener
        process.nextTick(() => {
          res.emit('error', err)
        })
      }).then(app => {
        if (app) {
          const renderStream = renderer.renderToStream(app, context)

          renderStream.on('error', err => {
            rewriteErrorTrace(err, maps)
            res.emit('error', err)
          })

          // relay HTMLStream special events
          if (rendererOptions && rendererOptions.template) {
            renderStream.on('beforeStart', () => {
              res.emit('beforeStart')
            })
            renderStream.on('beforeEnd', () => {
              res.emit('beforeEnd')
            })
          }

          renderStream.pipe(res)
        }
      })

      return res
    }
  }
  }
}
